/*-------------------------------------------------------------------------
 *
 * prepunion.c
 *	  规划集合操作查询的例程。文件名是一个遗留，
 *	  在只有 UNION 被实现的时代。
 *
 * 规划器中有两条处理集合操作查询的代码路径。
 * 如果一个子查询完全由简单的 UNION ALL 操作组成，
 * 它会被转换为“附加关系”。否则，它将由本模块中的
 * 通用代码处理（plan_set_operations 及其子例程）。这里对
 * 附加关系情况有一些支持代码，但大多数繁重工作在其他地方
 * 完成，尤其是在 prepjointree.c 和 allpaths.c 中。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/prep/prepunion.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/htup_details.h"
#include "access/sysattr.h"
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_type.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/planmain.h"
#include "optimizer/planner.h"
#include "optimizer/prep.h"
#include "optimizer/tlist.h"
#include "parser/parse_coerce.h"
#include "parser/parsetree.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/selfuncs.h"
#include "utils/syscache.h"


static RelOptInfo *fc_recurse_set_operations(Node *fc_setOp, PlannerInfo *fc_root,
										  List *fc_colTypes, List *fc_colCollations,
										  bool fc_junkOK,
										  int fc_flag, List *fc_refnames_tlist,
										  List **fc_pTargetList,
										  double *fc_pNumGroups);
static RelOptInfo *fc_generate_recursion_path(SetOperationStmt *fc_setOp,
										   PlannerInfo *fc_root,
										   List *fc_refnames_tlist,
										   List **fc_pTargetList);
static RelOptInfo *fc_generate_union_paths(SetOperationStmt *fc_op, PlannerInfo *fc_root,
										List *fc_refnames_tlist,
										List **fc_pTargetList);
static RelOptInfo *fc_generate_nonunion_paths(SetOperationStmt *fc_op, PlannerInfo *fc_root,
										   List *fc_refnames_tlist,
										   List **fc_pTargetList);
static List *fc_plan_union_children(PlannerInfo *fc_root,
								 SetOperationStmt *fc_top_union,
								 List *fc_refnames_tlist,
								 List **fc_tlist_list);
static Path *fc_make_union_unique(SetOperationStmt *fc_op, Path *fc_path, List *fc_tlist,
							   PlannerInfo *fc_root);
static void fc_postprocess_setop_rel(PlannerInfo *fc_root, RelOptInfo *fc_rel);
static bool fc_choose_hashed_setop(PlannerInfo *fc_root, List *fc_groupClauses,
								Path *fc_input_path,
								double fc_dNumGroups, double fc_dNumOutputRows,
								const char *fc_construct);
static List *fc_generate_setop_tlist(List *fc_colTypes, List *fc_colCollations,
								  int fc_flag,
								  Index fc_varno,
								  bool fc_hack_constants,
								  List *fc_input_tlist,
								  List *fc_refnames_tlist);
static List *fc_generate_append_tlist(List *fc_colTypes, List *fc_colCollations,
								   bool fc_flag,
								   List *fc_input_tlists,
								   List *fc_refnames_tlist);
static List *fc_generate_setop_grouplist(SetOperationStmt *fc_op, List *fc_targetlist);


/*
 * plan_set_operations
 *
 *	  计划用于一组集合运算 (UNION/INTERSECT/EXCEPT) 的查询树
 *
 * 本例程仅处理给定查询的 setOperations 树。
 * 在我们返回到 grouping_planner 时，任何请求的顶层 ORDER BY 将被处理；
 * LIMIT 也是如此。
 *
 * 我们返回的是一个 "upperrel" RelOptInfo，包含至少一个实现集合操作树的 Path。
 * 此外，root->processed_tlist 接收代表最上方 setop 节点输出的目标列表。
 */
RelOptInfo * plan_set_operations(PlannerInfo *fc_root)
{
	Query	   *fc_parse = fc_root->parse;
	SetOperationStmt *fc_topop = castNode(SetOperationStmt, fc_parse->setOperations);
	Node	   *fc_node;
	RangeTblEntry *fc_leftmostRTE;
	Query	   *fc_leftmostQuery;
	RelOptInfo *fc_setop_rel;
	List	   *fc_top_tlist;

	Assert(fc_topop);

	/* 检查是否存在不支持的内容 */
	Assert(fc_parse->jointree->fromlist == NIL);
	Assert(fc_parse->jointree->quals == NULL);
	Assert(fc_parse->groupClause == NIL);
	Assert(fc_parse->havingQual == NULL);
	Assert(fc_parse->windowClause == NIL);
	Assert(fc_parse->distinctClause == NIL);

	/*
	 * 在外部查询级别，我们将没有任何真正的等价关系要处理；
	 * 但我们确实希望能够生成 pathkeys，这将需要
	 * 单一成员的 EquivalenceClasses。表明 EC 合并已完成
	 *，以便 pathkeys.c 不会抱怨。
	 */
	Assert(fc_root->eq_classes == NIL);
	fc_root->ec_merging_done = true;

	/*
	 * 我们需要为每个叶子子查询构建 RelOptInfos，这些查询是在此查询中的 RTE_SUBQUERY rangetable 条目。准备它们的索引
	 * 数组，以及在需要时用于 AppendRelInfos。
	 */
	setup_simple_rel_arrays(fc_root);

	/*
	 * 找到最左侧的组件查询。我们需要使用其列名为
	 * 所有生成的 tlists（否则 SELECT INTO 将无法正常工作）。
	 */
	fc_node = fc_topop->larg;
	while (fc_node && IsA(fc_node, SetOperationStmt))
		fc_node = ((SetOperationStmt *) fc_node)->larg;
	Assert(fc_node && IsA(fc_node, RangeTblRef));
	fc_leftmostRTE = fc_root->simple_rte_array[((RangeTblRef *) fc_node)->rtindex];
	fc_leftmostQuery = fc_leftmostRTE->subquery;
	Assert(fc_leftmostQuery != NULL);

	/*
	 * 如果最上面的节点是递归联合，则需要进行特殊处理。
	 */
	if (fc_root->hasRecursion)
	{
		fc_setop_rel = fc_generate_recursion_path(fc_topop, fc_root,
											fc_leftmostQuery->targetList,
											&fc_top_tlist);
	}
	else
	{
		
/*
		 * 递归处理 setOperations 树以生成 set ops 的路径。最终输出路径应仅具有
		 * 顶层节点显示的列类型，加上可能的 resjunk 工作列（我们可以依赖上层节点来处理）。
		 */
		fc_setop_rel = fc_recurse_set_operations((Node *) fc_topop, fc_root,
										   fc_topop->colTypes, fc_topop->colCollations,
										   true, -1,
										   fc_leftmostQuery->targetList,
										   &fc_top_tlist,
										   NULL);
	}

	/* 必须将构建的 tlist 返回到 root->processed_tlist。 */
	fc_root->processed_tlist = fc_top_tlist;

	return fc_setop_rel;
}

/*
 * recurse_set_operations
 *	  递归处理 set operations 树中的一步
 *
 * colTypes: set-op 结果列数据类型的 OID 列表
 * colCollations: set-op 结果列排序的 OID 列表
 * junkOK: 如果为真，子级 resjunk 列可以留在结果中
 * flag: 如果 >= 0，添加一个 resjunk 输出列表示 flag 的值
 * refnames_tlist: 从中获取列名的目标列表
 *
 * 返回子树的 RelOptInfo 及这些输出参数：
 * *pTargetList: 接收子树顶级计划的完整 tlist
 * *pNumGroups: 如果不为 NULL，我们估计结果中不同组的数量，并存储在此处
 *
 * pTargetList 输出参数在很大程度上与返回的 RelOptInfo 的 pathtarget 重复，
 * 但目前我们需要它，因为此文件中的许多逻辑依赖于标记为 resjunk 的 flag 列。
 * 在重新设计其工作方式之前，这是简单的解决方案。
 *
 * 我们在这里不必担心 typmods：set-op 输入和输出 typmods 之间允许的唯一差异
 * 是输入是一个特定的 typmod，而输出是 -1，这不需要强制转换。
 */
static RelOptInfo * fc_recurse_set_operations(Node *fc_setOp, PlannerInfo *fc_root,
					   List *fc_colTypes, List *fc_colCollations,
					   bool fc_junkOK,
					   int fc_flag, List *fc_refnames_tlist,
					   List **fc_pTargetList,
					   double *fc_pNumGroups)
{
	RelOptInfo *fc_rel = NULL;		/* 保持编译器安静 */

	/* 防止因过于复杂的 setop 嵌套导致的堆栈溢出 */
	check_stack_depth();

	if (IsA(fc_setOp, RangeTblRef))
	{
		RangeTblRef *fc_rtr = (RangeTblRef *) fc_setOp;
		RangeTblEntry *fc_rte = fc_root->simple_rte_array[fc_rtr->rtindex];
		Query	   *fc_subquery = fc_rte->subquery;
		PlannerInfo *fc_subroot;
		RelOptInfo *fc_final_rel;
		Path	   *fc_subpath;
		Path	   *fc_path;
		List	   *fc_tlist;

		Assert(fc_subquery != NULL);

		/* 为此叶子子查询构建 RelOptInfo。 */
		fc_rel = build_simple_rel(fc_root, fc_rtr->rtindex, NULL);

		/* plan_params 不应该在当前查询级别中使用 */
		Assert(fc_root->plan_params == NIL);

		/* 为子查询生成 subroot 和路径 */
		fc_subroot = fc_rel->subroot = subquery_planner(fc_root->glob, fc_subquery,
												  fc_root,
												  false,
												  fc_root->tuple_fraction);

		/*
		 * 原始查询不应该包含对 setop 树中其他原始查询的任何交叉引用。
		 */
		if (fc_root->plan_params)
			elog(ERROR, "unexpected outer reference in set operation subquery");

		/* 找出这个子查询的适当目标列表。 */
		fc_tlist = fc_generate_setop_tlist(fc_colTypes, fc_colCollations,
									 fc_flag,
									 fc_rtr->rtindex,
									 true,
									 fc_subroot->processed_tlist,
									 fc_refnames_tlist);
		fc_rel->reltarget = create_pathtarget(fc_root, fc_tlist);

		/* 也将完整的 tlist 返回给调用者 */
		*fc_pTargetList = fc_tlist;

		/*
		 * 标记 rel 的估计输出行、宽度等。请注意，我们必须在生成外部查询路径之前
		 * 执行此操作，否则 cost_subqueryscan 会不满意。
		 */
		set_subquery_size_estimates(fc_root, fc_rel);

		/*
		 * 由于我们可能想要向此关系添加部分路径，因此必须正确设置其 consider_parallel 标志。
		 */
		fc_final_rel = fetch_upper_rel(fc_subroot, UPPERREL_FINAL, NULL);
		fc_rel->consider_parallel = fc_final_rel->consider_parallel;

		/*
		 * 目前，我们只考虑子查询的单个路径。
		 * 这很快就会改变（让它看起来更像
		 * set_subquery_pathlist）。
		 */
		fc_subpath = get_cheapest_fractional_path(fc_final_rel,
											   fc_root->tuple_fraction);

		/*
		 * 在其上添加一个 SubqueryScanPath。
		 *
		 * 我们不想去确定子查询的输出顺序，因为
		 * 这不会反映在集合操作结果中；所以只是给
		 * SubqueryScanPath 标记 nil pathkeys。 （XXX 这也应该很快改变。）
		 */
		fc_path = (Path *) create_subqueryscan_path(fc_root, fc_rel, fc_subpath,
												 NIL, NULL);

		add_path(fc_rel, fc_path);

		/*
		 * 如果我们有子关系的部分路径，我们可以用它
		 * 来为该关系构建一个部分路径。但没有必要考量
		 * 除了最便宜的路径之外的任何路径。
		 */
		if (fc_rel->consider_parallel && bms_is_empty(fc_rel->lateral_relids) &&
			fc_final_rel->partial_pathlist != NIL)
		{
			Path	   *fc_partial_subpath;
			Path	   *fc_partial_path;

			fc_partial_subpath = linitial(fc_final_rel->partial_pathlist);
			fc_partial_path = (Path *)
				create_subqueryscan_path(fc_root, fc_rel, fc_partial_subpath,
										 NIL, NULL);
			add_partial_path(fc_rel, fc_partial_path);
		}

		/*
		 * 如果调用者希望，估算组的数量。如果子查询使用了
		 * 分组或聚合，其输出可能大部分都是唯一的；
		 * 否则进行统计估算。
		 *
		 * XXX 你其实不想知道这个：我们使用子查询的原始
		 * targetlist 表达式进行估算，而不是 subroot->processed_tlist，
		 * 后者可能看起来更合适。原因是如果子查询本身是一个
		 * 集合操作，它可能返回一个包含由 generate_append_tlist
		 * 生成的 "varno 0" 变量的 processed_tlist，这会大大困扰
		 * estimate_num_groups。我们应该去掉 "varno 0" 的黑客
		 * 手段，但这需要重新设计集合操作的解析树表示，
		 * 以便每个集合操作的输出都可以对应一个 RTE。
		 */
		if (fc_pNumGroups)
		{
			if (fc_subquery->groupClause || fc_subquery->groupingSets ||
				fc_subquery->distinctClause ||
				fc_subroot->hasHavingQual || fc_subquery->hasAggs)
				*fc_pNumGroups = fc_subpath->rows;
			else
				*fc_pNumGroups = estimate_num_groups(fc_subroot,
												  get_tlist_exprs(fc_subquery->targetList, false),
												  fc_subpath->rows,
												  NULL,
												  NULL);
		}
	}
	else if (IsA(fc_setOp, SetOperationStmt))
	{
		SetOperationStmt *fc_op = (SetOperationStmt *) fc_setOp;

		/* UNION 与 INTERSECT/EXCEPT 很不相同 */
		if (fc_op->op == SETOP_UNION)
			fc_rel = fc_generate_union_paths(fc_op, fc_root,
									   fc_refnames_tlist,
									   fc_pTargetList);
		else
			fc_rel = fc_generate_nonunion_paths(fc_op, fc_root,
										  fc_refnames_tlist,
										  fc_pTargetList);
		if (fc_pNumGroups)
			*fc_pNumGroups = fc_rel->rows;

		/*
		 * 如果需要，添加一个 Result 节点以投影调用者请求的
		 * 输出列。
		 *
		 * XXX 你其实不想知道这个：setrefs.c 将对 Result 节点的
		 * tlist 应用 fix_upper_expr()。如果由 generate_setop_tlist()
		 * 生成的变量与子计划的相应 tlist 条目不完全相等，这将失败。
		 * 然而，由于子计划是由 generate_union_paths() 或
		 * generate_nonunion_paths() 生成的，因此它的 tlist 是由
		 * generate_append_tlist() 生成的，这样就可以正常工作。
		 * 我们只是告诉 generate_setop_tlist() 使用 varno 0。
		 */
		if (fc_flag >= 0 ||
			!tlist_same_datatypes(*fc_pTargetList, fc_colTypes, fc_junkOK) ||
			!tlist_same_collations(*fc_pTargetList, fc_colCollations, fc_junkOK))
		{
			PathTarget *fc_target;
			ListCell   *fc_lc;

			*fc_pTargetList = fc_generate_setop_tlist(fc_colTypes, fc_colCollations,
												fc_flag,
												0,
												false,
												*fc_pTargetList,
												fc_refnames_tlist);
			fc_target = create_pathtarget(fc_root, *fc_pTargetList);

			/* 对每条路径应用投影 */
			foreach(fc_lc, fc_rel->pathlist)
			{
				Path	   *fc_subpath = (Path *) lfirst(fc_lc);
				Path	   *fc_path;

				Assert(fc_subpath->param_info == NULL);
				fc_path = apply_projection_to_path(fc_root, fc_subpath->parent,
												fc_subpath, fc_target);
				/* 如果我们必须添加 Result，路径就与子路径不同 */
				if (fc_path != fc_subpath)
					lfirst(fc_lc) = fc_path;
			}

			/* 对每个部分路径应用投影 */
			foreach(fc_lc, fc_rel->partial_pathlist)
			{
				Path	   *fc_subpath = (Path *) lfirst(fc_lc);
				Path	   *fc_path;

				Assert(fc_subpath->param_info == NULL);

				/* 避免对路径应用投影，以防多重引用 */
				fc_path = (Path *) create_projection_path(fc_root, fc_subpath->parent,
													   fc_subpath, fc_target);
				lfirst(fc_lc) = fc_path;
			}
		}
	}
	else
	{
		elog(ERROR, "unrecognized node type: %d",
			 (int) nodeTag(fc_setOp));
		*fc_pTargetList = NIL;
	}

	fc_postprocess_setop_rel(fc_root, fc_rel);

	return fc_rel;
}

/*
 * 为递归 UNION 节点生成路径
 */
static RelOptInfo * fc_generate_recursion_path(SetOperationStmt *fc_setOp, PlannerInfo *fc_root,
						List *fc_refnames_tlist,
						List **fc_pTargetList)
{
	RelOptInfo *fc_result_rel;
	Path	   *fc_path;
	RelOptInfo *fc_lrel,
			   *fc_rrel;
	Path	   *fc_lpath;
	Path	   *fc_rpath;
	List	   *fc_lpath_tlist;
	List	   *fc_rpath_tlist;
	List	   *fc_tlist;
	List	   *fc_groupList;
	double		fc_dNumGroups;

	/* 解析器应该已经拒绝其他情况 */
	if (fc_setOp->op != SETOP_UNION)
		elog(ERROR, "only UNION queries can be recursive");
	/* 工作表 ID 应该被分配 */
	Assert(fc_root->wt_param_id >= 0);

	/*
	 * 与常规的 UNION 节点不同，分别处理左输入和右输入
	 * 而不打算将它们合并为一个 Append。
	 */
	fc_lrel = fc_recurse_set_operations(fc_setOp->larg, fc_root,
								  fc_setOp->colTypes, fc_setOp->colCollations,
								  false, -1,
								  fc_refnames_tlist,
								  &fc_lpath_tlist,
								  NULL);
	fc_lpath = fc_lrel->cheapest_total_path;
	/* 右路径将希望查看左路径 ... */
	fc_root->non_recursive_path = fc_lpath;
	fc_rrel = fc_recurse_set_operations(fc_setOp->rarg, fc_root,
								  fc_setOp->colTypes, fc_setOp->colCollations,
								  false, -1,
								  fc_refnames_tlist,
								  &fc_rpath_tlist,
								  NULL);
	fc_rpath = fc_rrel->cheapest_total_path;
	fc_root->non_recursive_path = NULL;

	/*
	 * 为 RecursiveUnion 路径节点生成 tlist --- 与 Append 情况相同
	 */
	fc_tlist = fc_generate_append_tlist(fc_setOp->colTypes, fc_setOp->colCollations, false,
								  list_make2(fc_lpath_tlist, fc_rpath_tlist),
								  fc_refnames_tlist);

	*fc_pTargetList = fc_tlist;

	/* 构建结果关系。 */
	fc_result_rel = fetch_upper_rel(fc_root, UPPERREL_SETOP,
								 bms_union(fc_lrel->relids, fc_rrel->relids));
	fc_result_rel->reltarget = create_pathtarget(fc_root, fc_tlist);

	/*
	 * 如果是 UNION，识别分组操作符
	 */
	if (fc_setOp->all)
	{
		fc_groupList = NIL;
		fc_dNumGroups = 0;
	}
	else
	{
		/* 识别分组语义 */
		fc_groupList = fc_generate_setop_grouplist(fc_setOp, fc_tlist);

		/* 我们这里只支持哈希 */
		if (!grouping_is_hashable(fc_groupList))
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("could not implement recursive UNION"),
					 errdetail("All column datatypes must be hashable.")));

		/*
		 * 目前，将不同组的数量视为
		 * 总输入大小，即最坏情况。
		 */
		fc_dNumGroups = fc_lpath->rows + fc_rpath->rows * 10;
	}

	/*
	 * 并创建路径节点。
	 */
	fc_path = (Path *) create_recursiveunion_path(fc_root,
											   fc_result_rel,
											   fc_lpath,
											   fc_rpath,
											   fc_result_rel->reltarget,
											   fc_groupList,
											   fc_root->wt_param_id,
											   fc_dNumGroups);

	add_path(fc_result_rel, fc_path);
	fc_postprocess_setop_rel(fc_root, fc_result_rel);
	return fc_result_rel;
}

/*
 * 为 UNION 或 UNION ALL 节点生成路径
 */
static RelOptInfo * fc_generate_union_paths(SetOperationStmt *fc_op, PlannerInfo *fc_root,
					 List *fc_refnames_tlist,
					 List **fc_pTargetList)
{
	Relids		fc_relids = NULL;
	RelOptInfo *fc_result_rel;
	double		fc_save_fraction = fc_root->tuple_fraction;
	ListCell   *fc_lc;
	List	   *fc_pathlist = NIL;
	List	   *fc_partial_pathlist = NIL;
	bool		fc_partial_paths_valid = true;
	bool		fc_consider_parallel = true;
	List	   *fc_rellist;
	List	   *fc_tlist_list;
	List	   *fc_tlist;
	Path	   *fc_path;

	/*
	 * 如果是简单的 UNION，告诉子节点获取所有元组。
	 *
	 * 注意：在 UNION ALL 中，我们未修改地将 top-level tuple_fraction
	 * 传递给 UNION ALL 的每个分支。可以提出减少后续分支的元组
	 * 比例（折扣早期分支结果的预期大小），但似乎不值得麻烦。正常
	 * 情况是顶层没有零的元组比例，并且按原样传递通常足以获得
	 * 优先快速启动计划的期望结果。
	 */
	if (!fc_op->all)
		fc_root->tuple_fraction = 0.0;

	/*
	 * 如果我的任何子节点是相同的 UNION 节点（相同操作、全标志
	 * 和 colTypes/colCollations），那么它们可以合并到这个节点中，
	 * 这样我们只生成一个 Append 和唯一化处理。而且递归查找
	 * 这样的节点并计算它们的子路径。
	 */
	fc_rellist = fc_plan_union_children(fc_root, fc_op, fc_refnames_tlist, &fc_tlist_list);

	/*
	 * 为 Append 计划节点生成 tlist。
	 *
	 * Append 计划的 tlist 对于 Append 来说并不重要，
	 * 但我们必须让它看起来真实，以便为下一层计划提供帮助。
	 */
	fc_tlist = fc_generate_append_tlist(fc_op->colTypes, fc_op->colCollations, false,
								  fc_tlist_list, fc_refnames_tlist);

	*fc_pTargetList = fc_tlist;

	/* 构建路径列表和 relid 集。 */
	foreach(fc_lc, fc_rellist)
	{
		RelOptInfo *fc_rel = lfirst(fc_lc);

		fc_pathlist = lappend(fc_pathlist, fc_rel->cheapest_total_path);

		if (fc_consider_parallel)
		{
			if (!fc_rel->consider_parallel)
			{
				fc_consider_parallel = false;
				fc_partial_paths_valid = false;
			}
			else if (fc_rel->partial_pathlist == NIL)
				fc_partial_paths_valid = false;
			else
				fc_partial_pathlist = lappend(fc_partial_pathlist,
										   linitial(fc_rel->partial_pathlist));
		}

		fc_relids = bms_union(fc_relids, fc_rel->relids);
	}

	/* 构建结果关系。 */
	fc_result_rel = fetch_upper_rel(fc_root, UPPERREL_SETOP, fc_relids);
	fc_result_rel->reltarget = create_pathtarget(fc_root, fc_tlist);
	fc_result_rel->consider_parallel = fc_consider_parallel;

	/*
	 * 将子结果连接在一起。
	 */
	fc_path = (Path *) create_append_path(fc_root, fc_result_rel, fc_pathlist, NIL,
									   NIL, NULL, 0, false, -1);

	/*
	 * 对于 UNION ALL，我们只需要 Append 路径。对于 UNION，需要
	 * 添加节点以去除重复项。
	 */
	if (!fc_op->all)
		fc_path = fc_make_union_unique(fc_op, fc_path, fc_tlist, fc_root);

	add_path(fc_result_rel, fc_path);

	/*
	 * 估算组的数量。目前我们只是假设输出是唯一的
	 * --- 这对于 UNION 情况肯定是正确的，我们还是想要最坏情况的
	 * 估算。
	 */
	fc_result_rel->rows = fc_path->rows;

	/*
	 * 现在考虑使用部分路径加 Append 加 Gather 来做同样的事情。
	 */
	if (fc_partial_paths_valid)
	{
		Path	   *fc_ppath;
		ListCell   *fc_lc;
		int			fc_parallel_workers = 0;

		/* 找到任何子路径请求的最高工作者数量。 */
		foreach(fc_lc, fc_partial_pathlist)
		{
			Path	   *fc_path = lfirst(fc_lc);

			fc_parallel_workers = Max(fc_parallel_workers, fc_path->parallel_workers);
		}
		Assert(fc_parallel_workers > 0);

		/*
		 * 如果允许使用并行追加，始终请求至少
		 * log2（孩子数量）条路径。我们假设在这种情况下，拥有额外的工作者会有用，
		 * 因为它们将分散在孩子之间。精确的公式只是个猜测；请参见
		 * add_paths_to_append_rel。
		 */
		if (enable_parallel_append)
		{
			fc_parallel_workers = Max(fc_parallel_workers,
								   fls(list_length(fc_partial_pathlist)));
			fc_parallel_workers = Min(fc_parallel_workers,
								   max_parallel_workers_per_gather);
		}
		Assert(fc_parallel_workers > 0);

		fc_ppath = (Path *)
			create_append_path(fc_root, fc_result_rel, NIL, fc_partial_pathlist,
							   NIL, NULL,
							   fc_parallel_workers, enable_parallel_append,
							   -1);
		fc_ppath = (Path *)
			create_gather_path(fc_root, fc_result_rel, fc_ppath,
							   fc_result_rel->reltarget, NULL, NULL);
		if (!fc_op->all)
			fc_ppath = fc_make_union_unique(fc_op, fc_ppath, fc_tlist, fc_root);
		add_path(fc_result_rel, fc_ppath);
	}

	/* 取消可能强制 tuple_fraction 为 0 的效果 */
	fc_root->tuple_fraction = fc_save_fraction;

	return fc_result_rel;
}

/*
 * 为 INTERSECT、INTERSECT ALL、EXCEPT 或 EXCEPT ALL 节点生成路径
 */
static RelOptInfo * fc_generate_nonunion_paths(SetOperationStmt *fc_op, PlannerInfo *fc_root,
						List *fc_refnames_tlist,
						List **fc_pTargetList)
{
	RelOptInfo *fc_result_rel;
	RelOptInfo *fc_lrel,
			   *fc_rrel;
	double		fc_save_fraction = fc_root->tuple_fraction;
	Path	   *fc_lpath,
			   *fc_rpath,
			   *fc_path;
	List	   *fc_lpath_tlist,
			   *fc_rpath_tlist,
			   *fc_tlist_list,
			   *fc_tlist,
			   *fc_groupList,
			   *fc_pathlist;
	double		fc_dLeftGroups,
				fc_dRightGroups,
				fc_dNumGroups,
				fc_dNumOutputRows;
	bool		fc_use_hash;
	SetOpCmd	fc_cmd;
	int			fc_firstFlag;

	/*
	 * 告诉孩子们获取所有元组。
	 */
	fc_root->tuple_fraction = 0.0;

	/* 递归到孩子们，确保它们的输出被标记 */
	fc_lrel = fc_recurse_set_operations(fc_op->larg, fc_root,
								  fc_op->colTypes, fc_op->colCollations,
								  false, 0,
								  fc_refnames_tlist,
								  &fc_lpath_tlist,
								  &fc_dLeftGroups);
	fc_lpath = fc_lrel->cheapest_total_path;
	fc_rrel = fc_recurse_set_operations(fc_op->rarg, fc_root,
								  fc_op->colTypes, fc_op->colCollations,
								  false, 1,
								  fc_refnames_tlist,
								  &fc_rpath_tlist,
								  &fc_dRightGroups);
	fc_rpath = fc_rrel->cheapest_total_path;

	/* 取消强制 tuple_fraction 为 0 的效果 */
	fc_root->tuple_fraction = fc_save_fraction;

	/*
	 * 对于 EXCEPT，我们必须先放置左侧输入。对于 INTERSECT，任意
	 * 顺序都应给出相同结果，我们更倾向于先放置较小的
	 * 输入，以便在哈希情况下最小化哈希表的大小。“较小”意味着具有更少组数的那个。
	 */
	if (fc_op->op == SETOP_EXCEPT || fc_dLeftGroups <= fc_dRightGroups)
	{
		fc_pathlist = list_make2(fc_lpath, fc_rpath);
		fc_tlist_list = list_make2(fc_lpath_tlist, fc_rpath_tlist);
		fc_firstFlag = 0;
	}
	else
	{
		fc_pathlist = list_make2(fc_rpath, fc_lpath);
		fc_tlist_list = list_make2(fc_rpath_tlist, fc_lpath_tlist);
		fc_firstFlag = 1;
	}

	/*
	 * 为 Append 计划节点生成 tlist。
	 *
	 * 在 Append 关注的方面，Append 计划的 tlist 并不重要，但为了下一个计划层次的
	 * 方便，我们必须使其看起来真实。实际上，它必须足够真实，以至于标志
	 * 列显示为变量而非常量，否则 setrefs.c 会感到困惑。
	 */
	fc_tlist = fc_generate_append_tlist(fc_op->colTypes, fc_op->colCollations, true,
								  fc_tlist_list, fc_refnames_tlist);

	*fc_pTargetList = fc_tlist;

	/* 构建结果关系。 */
	fc_result_rel = fetch_upper_rel(fc_root, UPPERREL_SETOP,
								 bms_union(fc_lrel->relids, fc_rrel->relids));
	fc_result_rel->reltarget = create_pathtarget(fc_root, fc_tlist);

	/*
	 * 将子结果连接在一起。
	 */
	fc_path = (Path *) create_append_path(fc_root, fc_result_rel, fc_pathlist, NIL,
									   NIL, NULL, 0, false, -1);

	/* 识别分组语义 */
	fc_groupList = fc_generate_setop_grouplist(fc_op, fc_tlist);

	/*
	 * 估算我们需要的哈希表条目的不同组数；这是 EXCEPT 的左侧输入的大小，
	 * 或 INTERSECT 的较小输入。还要估计最终输出行的数量。
	 * 在非 ALL 的情况下，我们估计每个组生成一条输出行；在 ALL 的情况下使用相关关系大小。
	 * 这些当然是最坏情况下的估算，但我们需要保持保守。
	 */
	if (fc_op->op == SETOP_EXCEPT)
	{
		fc_dNumGroups = fc_dLeftGroups;
		fc_dNumOutputRows = fc_op->all ? fc_lpath->rows : fc_dNumGroups;
	}
	else
	{
		fc_dNumGroups = Min(fc_dLeftGroups, fc_dRightGroups);
		fc_dNumOutputRows = fc_op->all ? Min(fc_lpath->rows, fc_rpath->rows) : fc_dNumGroups;
	}

	/*
	 * 决定是哈希还是排序，并在需要时添加排序节点。
	 */
	fc_use_hash = fc_choose_hashed_setop(fc_root, fc_groupList, fc_path,
								   fc_dNumGroups, fc_dNumOutputRows,
								   (fc_op->op == SETOP_INTERSECT) ? "INTERSECT" : "EXCEPT");

	if (fc_groupList && !fc_use_hash)
		fc_path = (Path *) create_sort_path(fc_root,
										 fc_result_rel,
										 fc_path,
										 make_pathkeys_for_sortclauses(fc_root,
																	   fc_groupList,
																	   fc_tlist),
										 -1.0);

	/*
	 * 最后，添加一个 SetOp 路径节点以生成正确的输出。
	 */
	switch (fc_op->op)
	{
		case SETOP_INTERSECT:
			fc_cmd = fc_op->all ? SETOPCMD_INTERSECT_ALL : SETOPCMD_INTERSECT;
			break;
		case SETOP_EXCEPT:
			fc_cmd = fc_op->all ? SETOPCMD_EXCEPT_ALL : SETOPCMD_EXCEPT;
			break;
		default:
			elog(ERROR, "unrecognized set op: %d", (int) fc_op->op);
			fc_cmd = SETOPCMD_INTERSECT;	/* 保持编译器安静 */
			break;
	}
	fc_path = (Path *) create_setop_path(fc_root,
									  fc_result_rel,
									  fc_path,
									  fc_cmd,
									  fc_use_hash ? SETOP_HASHED : SETOP_SORTED,
									  fc_groupList,
									  list_length(fc_op->colTypes) + 1,
									  fc_use_hash ? fc_firstFlag : -1,
									  fc_dNumGroups,
									  fc_dNumOutputRows);

	fc_result_rel->rows = fc_path->rows;
	add_path(fc_result_rel, fc_path);
	return fc_result_rel;
}

/*
 * 提取相同属性的 UNION 节点的子节点，并在 N 方式 UNION 下进行查询规划。
 *
 * 结果是一个包含子节点路径的 RelOptInfos 列表，
 * 对于每个是叶查询或非相同的 setop 的后代有一个条目。
 * 我们还返回一个相应的包含孩子的目标列表的并行列表。
 *
 * 注意：我们也可以将 UNION ALL 提升到 UNION，因为不同的
 * 输出行反正会丢失。
 */
static List * fc_plan_union_children(PlannerInfo *fc_root,
					SetOperationStmt *fc_top_union,
					List *fc_refnames_tlist,
					List **fc_tlist_list)
{
	List	   *fc_pending_rels = list_make1(fc_top_union);
	List	   *fc_result = NIL;
	List	   *fc_child_tlist;

	*fc_tlist_list = NIL;

	while (fc_pending_rels != NIL)
	{
		Node	   *fc_setOp = linitial(fc_pending_rels);

		fc_pending_rels = list_delete_first(fc_pending_rels);

		if (IsA(fc_setOp, SetOperationStmt))
		{
			SetOperationStmt *fc_op = (SetOperationStmt *) fc_setOp;

			if (fc_op->op == fc_top_union->op &&
				(fc_op->all == fc_top_union->all || fc_op->all) &&
				equal(fc_op->colTypes, fc_top_union->colTypes) &&
				equal(fc_op->colCollations, fc_top_union->colCollations))
			{
				/* 相同的 UNION，因此将孩子折叠到父节点中 */
				fc_pending_rels = lcons(fc_op->rarg, fc_pending_rels);
				fc_pending_rels = lcons(fc_op->larg, fc_pending_rels);
				continue;
			}
		}

		/*
		 * 不相同，因此单独规划这个孩子。
		 *
		 * 注意，我们不允许孩子结果中有任何 resjunk 列。这是
		 * 必要的，因为实现并集的 Append 节点不会进行
		 * 任何投影，如果我们的某些输出元组有垃圾而有些没有，上层将会感到困惑。
		 * 该情况仅在我们有 EXCEPT 或 INTERSECT 作为子节点时出现，
		 * 否则不会有 resjunk。
		 */
		fc_result = lappend(fc_result, fc_recurse_set_operations(fc_setOp, fc_root,
														fc_top_union->colTypes,
														fc_top_union->colCollations,
														false, -1,
														fc_refnames_tlist,
														&fc_child_tlist,
														NULL));
		*fc_tlist_list = lappend(*fc_tlist_list, fc_child_tlist);
	}

	return fc_result;
}

/*
 * 向给定路径树添加节点，以唯一化 UNION 的结果。
 */
static Path * fc_make_union_unique(SetOperationStmt *fc_op, Path *fc_path, List *fc_tlist,
				  PlannerInfo *fc_root)
{
	RelOptInfo *fc_result_rel = fetch_upper_rel(fc_root, UPPERREL_SETOP, NULL);
	List	   *fc_groupList;
	double		fc_dNumGroups;

	/* 识别分组语义 */
	fc_groupList = fc_generate_setop_grouplist(fc_op, fc_tlist);

	/*
	 * XXX 目前，将不同组数视为总输入大小，即
	 * 最坏情况。这太保守了，但不清楚如何获得
	 * 真实大小的合理估计。还应注意初学者倾向于写 UNION 而不是
	 * UNION ALL，即使他们不期望任何重复......
	 */
	fc_dNumGroups = fc_path->rows;

	/* 决定是哈希还是排序 */
	if (fc_choose_hashed_setop(fc_root, fc_groupList, fc_path,
							fc_dNumGroups, fc_dNumGroups,
							"UNION"))
	{
		/* 哈希聚合计划——不需要排序 */
		fc_path = (Path *) create_agg_path(fc_root,
										fc_result_rel,
										fc_path,
										create_pathtarget(fc_root, fc_tlist),
										AGG_HASHED,
										AGGSPLIT_SIMPLE,
										fc_groupList,
										NIL,
										NULL,
										fc_dNumGroups);
	}
	else
	{
		/* 排序和唯一化 */
		if (fc_groupList)
			fc_path = (Path *)
				create_sort_path(fc_root,
								 fc_result_rel,
								 fc_path,
								 make_pathkeys_for_sortclauses(fc_root,
															   fc_groupList,
															   fc_tlist),
								 -1.0);
		fc_path = (Path *) create_upper_unique_path(fc_root,
												 fc_result_rel,
												 fc_path,
												 list_length(fc_path->pathkeys),
												 fc_dNumGroups);
	}

	return fc_path;
}

/*
 * postprocess_setop_rel - 在添加路径后执行所需步骤
 */
static void fc_postprocess_setop_rel(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	/*
	 * 我们目前不担心允许 FDW 为
	 * 此关系贡献路径，但给扩展一个机会。
	 */
	if (create_upper_paths_hook)
		(*create_upper_paths_hook) (fc_root, UPPERREL_SETOP,
									NULL, fc_rel, NULL);

	/* 选择最便宜的路径 */
	set_cheapest(fc_rel);
}

/*
 * choose_hashed_setop - 我们应该使用哈希进行集合操作吗？
 */
static bool fc_choose_hashed_setop(PlannerInfo *fc_root, List *fc_groupClauses,
					Path *fc_input_path,
					double fc_dNumGroups, double fc_dNumOutputRows,
					const char *fc_construct)
{
	int			fc_numGroupCols = list_length(fc_groupClauses);
	Size		fc_hash_mem_limit = get_hash_memory_limit();
	bool		fc_can_sort;
	bool		fc_can_hash;
	Size		fc_hashentrysize;
	Path		fc_hashed_p;
	Path		fc_sorted_p;
	double		fc_tuple_fraction;

	/* 检查操作符是否支持排序或哈希 */
	fc_can_sort = grouping_is_sortable(fc_groupClauses);
	fc_can_hash = grouping_is_hashable(fc_groupClauses);
	if (fc_can_hash && fc_can_sort)
	{
		/* 我们有一个有意义的选择要做，继续... */
	}
	else if (fc_can_hash)
		return true;
	else if (fc_can_sort)
		return false;
	else
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
		/* 翻译者：%s 是 UNION、INTERSECT 或 EXCEPT */
				 errmsg("could not implement %s", fc_construct),
				 errdetail("Some of the datatypes only support hashing, while others only support sorting.")));

	/* 当 enable_hashagg 关闭时优先使用排序 */
	if (!enable_hashagg)
		return false;

	/*
	 * 如果看起来哈希表不会适合 hash_mem，就不要这样做。
	 */
	fc_hashentrysize = MAXALIGN(fc_input_path->pathtarget->width) + MAXALIGN(SizeofMinimalTupleHeader);

	if (fc_hashentrysize * fc_dNumGroups > fc_hash_mem_limit)
		return false;

	/*
	 * 查看估计成本是否不超过以另一种方式进行操作的成本。
	 *
	 * 我们需要考虑 input_plan + hashagg 与 input_plan + sort +
	 * group。注意，实际结果计划可能涉及 SetOp 或
	 * Unique 节点，而不是 Agg 或 Group，但对于我们的目的来说，
	 * Agg 和 Group 的成本估算应该足够接近。
	 *
	 * 这些路径变量是虚拟的，仅用于存储成本字段；我们不
	 * 为这些步骤生成实际的路径。
	 */
	cost_agg(&fc_hashed_p, fc_root, AGG_HASHED, NULL,
			 fc_numGroupCols, fc_dNumGroups,
			 NIL,
			 fc_input_path->startup_cost, fc_input_path->total_cost,
			 fc_input_path->rows, fc_input_path->pathtarget->width);

	/*
	 * 现在处理排序的情况。注意，输入 *始终* 是未排序的，
	 * 因为它是通过将不相关的子关系附加在一起生成的。
	 */
	fc_sorted_p.startup_cost = fc_input_path->startup_cost;
	fc_sorted_p.total_cost = fc_input_path->total_cost;
	/* XXX cost_sort实际上并不关注pathkeys，因此只需传递NIL */
	cost_sort(&fc_sorted_p, fc_root, NIL, fc_sorted_p.total_cost,
			  fc_input_path->rows, fc_input_path->pathtarget->width,
			  0.0, work_mem, -1.0);
	cost_group(&fc_sorted_p, fc_root, fc_numGroupCols, fc_dNumGroups,
			   NIL,
			   fc_sorted_p.startup_cost, fc_sorted_p.total_cost,
			   fc_input_path->rows);

	/*
	 * 现在根据顶级元组分数做出决定。首先，我们必须将绝对计数（LIMIT）转换为分数形式。
	 */
	fc_tuple_fraction = fc_root->tuple_fraction;
	if (fc_tuple_fraction >= 1.0)
		fc_tuple_fraction /= fc_dNumOutputRows;

	if (compare_fractional_path_costs(&fc_hashed_p, &fc_sorted_p,
									  fc_tuple_fraction) < 0)
	{
		/* 哈希更便宜，因此使用它 */
		return true;
	}
	return false;
}

/*
 * 为集操作计划节点生成目标列表
 *
 * colTypes: 集操作结果列数据类型的OID列表
 * colCollations: 集操作结果列排序规则的OID列表
 * flag: 如果不需要标志列则为-1，创建常量标志列则为0或1
 * varno: 用于生成的Vars的varno
 * hack_constants: true表示复制常量（请参阅代码中的注释）
 * input_tlist: 此节点输入节点的目标列表
 * refnames_tlist: 用于提取列名的目标列表
 */
static List * fc_generate_setop_tlist(List *fc_colTypes, List *fc_colCollations,
					 int fc_flag,
					 Index fc_varno,
					 bool fc_hack_constants,
					 List *fc_input_tlist,
					 List *fc_refnames_tlist)
{
	List	   *fc_tlist = NIL;
	int			fc_resno = 1;
	ListCell   *fc_ctlc,
			   *fc_cclc,
			   *fc_itlc,
			   *fc_rtlc;
	TargetEntry *fc_tle;
	Node	   *fc_expr;

	forfour(fc_ctlc, fc_colTypes, fc_cclc, fc_colCollations,
			fc_itlc, fc_input_tlist, fc_rtlc, fc_refnames_tlist)
	{
		Oid			fc_colType = lfirst_oid(fc_ctlc);
		Oid			fc_colColl = lfirst_oid(fc_cclc);
		TargetEntry *fc_inputtle = (TargetEntry *) lfirst(fc_itlc);
		TargetEntry *fc_reftle = (TargetEntry *) lfirst(fc_rtlc);

		Assert(fc_inputtle->resno == fc_resno);
		Assert(fc_reftle->resno == fc_resno);
		Assert(!fc_inputtle->resjunk);
		Assert(!fc_reftle->resjunk);

		/*
		 * 生成引用输入列并具有适当数据类型和列名的列。必要时插入数据类型强制转换。
		 *
		 * 黑客：输入的目标列表中的常量照原样复制上来，而不是被引用为子查询输出。这主要是为了确保当我们试图将它们强制转换为输出列的数据类型时，未知常量的处理是正确的。但这仅在子查询扫描计划的第一级进行；我们不希望伪常量出现在上层节点的输出目标列表中！
		 */
		if (fc_hack_constants && fc_inputtle->expr && IsA(fc_inputtle->expr, Const))
			fc_expr = (Node *) fc_inputtle->expr;
		else
			fc_expr = (Node *) makeVar(fc_varno,
									fc_inputtle->resno,
									exprType((Node *) fc_inputtle->expr),
									exprTypmod((Node *) fc_inputtle->expr),
									exprCollation((Node *) fc_inputtle->expr),
									0);

		if (exprType(fc_expr) != fc_colType)
		{
			/*
			 * 注意：在这里应用coerce_to_common_type并不是很好；一个显著的点是，assign_expr_collations从未在任何生成的节点上运行。此时这不是问题，因为我们强制在下面暴露的排序规则是正确的。不过，最好是在生成器开始时为每个集操作生成正确的输出目标列表。
			 */
			fc_expr = coerce_to_common_type(NULL,	/* 这里没有未知值 */
										 fc_expr,
										 fc_colType,
										 "UNION/INTERSECT/EXCEPT");
		}

		/*
		 * 确保tlist条目的暴露排序规则与集操作匹配。这是必要的，因为plan_set_operations()将结果排序报告为SortGroupClauses的列表，而这些排序条目本身不携带排序规则，而只是引用tlist条目。如果我们不显示正确的排序规则，则planner.c可能会在更高层次的查询中做出错误的事情。
		 *
		 * 注意，我们使用RelabelType，而不是CollateExpr，因为此表达式将在没有进一步处理的情况下到达执行器。
		 */
		if (exprCollation(fc_expr) != fc_colColl)
			fc_expr = applyRelabelType(fc_expr,
									exprType(fc_expr), exprTypmod(fc_expr), fc_colColl,
									COERCE_IMPLICIT_CAST, -1, false);

		fc_tle = makeTargetEntry((Expr *) fc_expr,
							  (AttrNumber) fc_resno++,
							  pstrdup(fc_reftle->resname),
							  false);

		/*
		 * 按约定，所有非resjunk列在setop树中的ressortgroupref等于它们的resno。在某些情况下，ref并不是必须的，但这是一种比稍后修改tlist更干净的方式。
		 */
		fc_tle->ressortgroupref = fc_tle->resno;

		fc_tlist = lappend(fc_tlist, fc_tle);
	}

	if (fc_flag >= 0)
	{
		/* 添加一个resjunk标志列 */
		/* 标志值是给定的常量 */
		fc_expr = (Node *) makeConst(INT4OID,
								  -1,
								  InvalidOid,
								  sizeof(int32),
								  Int32GetDatum(fc_flag),
								  false,
								  true);
		fc_tle = makeTargetEntry((Expr *) fc_expr,
							  (AttrNumber) fc_resno++,
							  pstrdup("flag"),
							  true);
		fc_tlist = lappend(fc_tlist, fc_tle);
	}

	return fc_tlist;
}

/*
 * 为集操作Append节点生成目标列表
 *
 * colTypes: 集操作结果列数据类型的OID列表
 * colCollations: 集操作结果列排序规则的OID列表
 * flag: true表示从子计划复制的标志列
 * input_tlists: Append的子计划的目标列表列表
 * refnames_tlist: 用于提取列名的目标列表
 *
 * Append的目标列表中的条目应该始终是简单的Vars；我们只需要确保它们具有正确的数据类型/typmods/排序规则。
 * Vars总是以varno 0生成。
 *
 * XXX varno-zero方法的问题在于set_pathtarget_cost_width无法为我们在此处制作的目标列表找出一个现实的宽度。但是，我们应该重构此代码以直接生成PathTarget。
 */
static List * fc_generate_append_tlist(List *fc_colTypes, List *fc_colCollations,
					  bool fc_flag,
					  List *fc_input_tlists,
					  List *fc_refnames_tlist)
{
	List	   *fc_tlist = NIL;
	int			fc_resno = 1;
	ListCell   *fc_curColType;
	ListCell   *fc_curColCollation;
	ListCell   *fc_ref_tl_item;
	int			fc_colindex;
	TargetEntry *fc_tle;
	Node	   *fc_expr;
	ListCell   *fc_tlistl;
	int32	   *fc_colTypmods;

	/*
	 * 首先提取要使用的typmods。
	 *
	 * 如果所有输入都在某一特定列的类型和typmod上达成一致，则使用该typmod；否则使用-1。
	 */
	fc_colTypmods = (int32 *) palloc(list_length(fc_colTypes) * sizeof(int32));

	foreach(fc_tlistl, fc_input_tlists)
	{
		List	   *fc_subtlist = (List *) lfirst(fc_tlistl);
		ListCell   *fc_subtlistl;

		fc_curColType = list_head(fc_colTypes);
		fc_colindex = 0;
		foreach(fc_subtlistl, fc_subtlist)
		{
			TargetEntry *fc_subtle = (TargetEntry *) lfirst(fc_subtlistl);

			if (fc_subtle->resjunk)
				continue;
			Assert(fc_curColType != NULL);
			if (exprType((Node *) fc_subtle->expr) == lfirst_oid(fc_curColType))
			{
				/* 如果是第一个子计划，复制typmod；否则进行比较 */
				int32		fc_subtypmod = exprTypmod((Node *) fc_subtle->expr);

				if (fc_tlistl == list_head(fc_input_tlists))
					fc_colTypmods[fc_colindex] = fc_subtypmod;
				else if (fc_subtypmod != fc_colTypmods[fc_colindex])
					fc_colTypmods[fc_colindex] = -1;
			}
			else
			{
				/* 类型不一致，因此强制typmod为-1 */
				fc_colTypmods[fc_colindex] = -1;
			}
			fc_curColType = lnext(fc_colTypes, fc_curColType);
			fc_colindex++;
		}
		Assert(fc_curColType == NULL);
	}

	/*
	 * 现在我们可以为Append构建目标列表。
	 */
	fc_colindex = 0;
	forthree(fc_curColType, fc_colTypes, fc_curColCollation, fc_colCollations,
			 fc_ref_tl_item, fc_refnames_tlist)
	{
		Oid			fc_colType = lfirst_oid(fc_curColType);
		int32		fc_colTypmod = fc_colTypmods[fc_colindex++];
		Oid			fc_colColl = lfirst_oid(fc_curColCollation);
		TargetEntry *fc_reftle = (TargetEntry *) lfirst(fc_ref_tl_item);

		Assert(fc_reftle->resno == fc_resno);
		Assert(!fc_reftle->resjunk);
		fc_expr = (Node *) makeVar(0,
								fc_resno,
								fc_colType,
								fc_colTypmod,
								fc_colColl,
								0);
		fc_tle = makeTargetEntry((Expr *) fc_expr,
							  (AttrNumber) fc_resno++,
							  pstrdup(fc_reftle->resname),
							  false);

		/*
		 * 按约定，所有非resjunk列在setop树中的ressortgroupref等于它们的resno。在某些情况下，ref并不是必须的，但这是一种比稍后修改tlist更干净的方式。
		 */
		fc_tle->ressortgroupref = fc_tle->resno;

		fc_tlist = lappend(fc_tlist, fc_tle);
	}

	if (fc_flag)
	{
		/* 添加一个resjunk标志列 */
		/* 标志值显示为从子计划复制上来 */
		fc_expr = (Node *) makeVar(0,
								fc_resno,
								INT4OID,
								-1,
								InvalidOid,
								0);
		fc_tle = makeTargetEntry((Expr *) fc_expr,
							  (AttrNumber) fc_resno++,
							  pstrdup("flag"),
							  true);
		fc_tlist = lappend(fc_tlist, fc_tle);
	}

	pfree(fc_colTypmods);

	return fc_tlist;
}

/*
 * generate_setop_grouplist
 *		构建一个SortGroupClause列表，定义集操作输出列的排序/分组属性。
 *
 * 解析分析已确定了属性并构建了合适的列表，除了条目没有设置sortgrouprefs，因为解析器输出表示并不包括每个集操作的目标列表。因此我们需要在此处复制该列表并安装适当的sortgrouprefs（从目标列表复制）。
 */
static List * fc_generate_setop_grouplist(SetOperationStmt *fc_op, List *fc_targetlist)
{
	List	   *fc_grouplist = copyObject(fc_op->groupClauses);
	ListCell   *fc_lg;
	ListCell   *fc_lt;

	fc_lg = list_head(fc_grouplist);
	foreach(fc_lt, fc_targetlist)
	{
		TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_lt);
		SortGroupClause *fc_sgc;

		if (fc_tle->resjunk)
		{
			/* resjunk列不应具有sortgrouprefs */
			Assert(fc_tle->ressortgroupref == 0);
			continue;			/* 忽略 resjunk 列 */
		}

		/* 非resjunk列应具有sortgroupref = resno */
		Assert(fc_tle->ressortgroupref == fc_tle->resno);

		/* 非冗余列应该有分组子句 */
		Assert(fc_lg != NULL);
		fc_sgc = (SortGroupClause *) lfirst(fc_lg);
		fc_lg = lnext(fc_grouplist, fc_lg);
		Assert(fc_sgc->tleSortGroupRef == 0);

		fc_sgc->tleSortGroupRef = fc_tle->ressortgroupref;
	}
	Assert(fc_lg == NULL);
	return fc_grouplist;
}
